#include <userver/logging/log_extra.hpp>

#include <stdexcept>
#include <unordered_set>

#include <fmt/format.h>
#include <boost/container/small_vector.hpp>
#include <boost/stacktrace.hpp>

#include <userver/logging/level.hpp>
#include <userver/logging/log.hpp>
#include <userver/utils/assert.hpp>
#include <userver/utils/flags.hpp>
#include <userver/utils/trivial_map.hpp>

#include <logging/log_extra_stacktrace.hpp>

USERVER_NAMESPACE_BEGIN

namespace logging {

namespace {

LogExtra GetStacktrace(utils::Flags<impl::LogExtraStacktraceFlags> flags) {
    LogExtra ret;
    if (impl::ShouldLogStacktrace()) {
        impl::ExtendLogExtraWithStacktrace(ret, flags);
    }
    return ret;
}

LogExtra GetStacktrace(logging::LoggerRef logger, utils::Flags<impl::LogExtraStacktraceFlags> flags) {
    logging::LogExtra extra_stacktrace;

    if (LoggerShouldLogStacktrace(logger)) {
        impl::ExtendLogExtraWithStacktrace(extra_stacktrace, flags);
    }

    return extra_stacktrace;
}

constexpr utils::TrivialSet kTechnicalKeys = [](auto selector) {
    return selector().Case("timestamp").Case("level").Case("module").Case("task_id").Case("thread_id").Case("text");
};

}  // namespace

LogExtra::LogExtra() noexcept = default;

LogExtra::LogExtra(const LogExtra&) = default;

// NOLINTNEXTLINE(performance-noexcept-move-constructor)
LogExtra::LogExtra(LogExtra&&) = default;

LogExtra::~LogExtra() = default;

// NOLINTNEXTLINE(performance-noexcept-move-constructor)
LogExtra& LogExtra::operator=(LogExtra&&) = default;

LogExtra& LogExtra::operator=(const LogExtra&) = default;

LogExtra::LogExtra(InitializerList initial, ExtendType extend_type) { Extend(initial, extend_type); }

void LogExtra::Extend(std::string key, Value value, ExtendType extend_type) {
    Extend(std::move(key), ProtectedValue(std::move(value), extend_type == ExtendType::kFrozen), extend_type);
}

void LogExtra::Extend(Pair extra, ExtendType extend_type) {
    Extend(std::move(extra.first), std::move(extra.second), extend_type);
}

void LogExtra::Extend(InitializerList extra, ExtendType extend_type) {
    static_assert(std::variant_size_v<Value> == std::variant_size_v<ValueView>, "Value and ValueView do not match");
    constexpr auto visitor = [](auto view_or_small) -> Value {
        if constexpr (std::is_same_v<decltype(view_or_small), impl::JsonStringViewForInitializerList>) {
            UASSERT(view_or_small.json_str || view_or_small.json_value);
            UASSERT(!view_or_small.json_str || !view_or_small.json_value);
            return JsonString{view_or_small.json_str ? *view_or_small.json_str : *view_or_small.json_value};
        } else if constexpr (std::is_same_v<decltype(view_or_small), std::string_view>) {
            return std::string{view_or_small};
        } else {
            return view_or_small;
        }
    };

    for (const auto& view : extra) {
        Extend(std::string{view.first}, std::visit(visitor, view.second), extend_type);
    }
}

void LogExtra::Extend(const LogExtra& extra) {
    if (extra_->empty())
        extra_ = extra.extra_;
    else
        ExtendRange(extra.extra_->begin(), extra.extra_->end());
}

void LogExtra::Extend(LogExtra&& extra) {
    if (extra_->empty()) {
        std::swap(extra_, extra.extra_);
    } else {
        for (Map::value_type& pair : *extra.extra_) Extend(std::move(pair.first), std::move(pair.second));
        extra.extra_->clear();
    }
}

LogExtra LogExtra::StacktraceNocache() noexcept { return GetStacktrace(impl::LogExtraStacktraceFlags::kNoCache); }

LogExtra LogExtra::StacktraceNocache(logging::LoggerRef logger) noexcept {
    return GetStacktrace(logger, impl::LogExtraStacktraceFlags::kNoCache);
}

LogExtra LogExtra::Stacktrace() noexcept { return GetStacktrace({}); }

LogExtra LogExtra::Stacktrace(logging::LoggerRef logger) noexcept { return GetStacktrace(logger, {}); }

const std::pair<LogExtra::Key, LogExtra::ProtectedValue>* LogExtra::Find(std::string_view key) const {
    for (const auto& it : *extra_)
        if (it.first == key) return &it;
    return nullptr;
}

std::pair<LogExtra::Key, LogExtra::ProtectedValue>* LogExtra::Find(std::string_view key) {
    for (auto& it : *extra_)
        if (it.first == key) return &it;
    return nullptr;
}

void LogExtra::SetFrozen(std::string_view key) {
    auto* it = Find(key);
    if (!it) throw std::runtime_error(fmt::format("can't set frozen for non-existing key '{}'", key));
    it->second.SetFrozen();
}

const LogExtra::Value& LogExtra::GetValue(std::string_view key) const {
    static const LogExtra::Value kEmpty{};
    const auto* it = Find(key);
    if (!it) return kEmpty;
    return it->second.GetValue();
}

void LogExtra::Extend(std::string key, ProtectedValue protected_value, ExtendType extend_type) {
    UINVARIANT(
        !kTechnicalKeys.Contains(key),
        fmt::format(
            "'{}' is one of the [{}] technical keys. Overwrite would "
            "produce incorrect logs",
            key,
            kTechnicalKeys.Describe()
        )
    );
    auto* it = Find(key);
    if (!it) {
        extra_->emplace_back(
            std::move(key),
            extend_type == ExtendType::kFrozen ? ProtectedValue(std::move(protected_value.GetValue()), true)
                                               : std::move(protected_value)
        );
    } else {
        it->second = extend_type == ExtendType::kFrozen ? ProtectedValue(std::move(protected_value.GetValue()), true)
                                                        : std::move(protected_value);
    }
}

void LogExtra::Extend(Map::value_type extra, ExtendType extend_type) {
    Extend(std::move(extra.first), std::move(extra.second), extend_type);
}

LogExtra::ProtectedValue::ProtectedValue(Value value, bool frozen) : value_(std::move(value)), frozen_(frozen) {}

LogExtra::ProtectedValue& LogExtra::ProtectedValue::operator=(const ProtectedValue& other) {
    if (this == &other) return *this;

    if (frozen_) return *this;
    value_ = other.GetValue();
    frozen_ = other.frozen_;
    return *this;
}

LogExtra::ProtectedValue& LogExtra::ProtectedValue::operator=(ProtectedValue&& other) noexcept {
    if (frozen_) return *this;
    value_ = std::move(other.GetValue());
    frozen_ = other.frozen_;
    return *this;
}

bool LogExtra::ProtectedValue::IsFrozen() const { return frozen_; }

void LogExtra::ProtectedValue::SetFrozen() { frozen_ = true; }

void LogExtra::ProtectedValue::AssignIgnoringFrozenness(LogExtra::ProtectedValue other) {
    frozen_ = false;
    *this = std::move(other);
}

const LogExtra kEmptyLogExtra;

}  // namespace logging

USERVER_NAMESPACE_END
